React 18 변경점

React 18 변경점

새로운 기능

  • 자동배치
  • Transition 긴급/전환 업데이트
  • 새로운 Suspense 기능
  • 새로운 클라이언트 및 서버 렌더링 API
  • 새로운 엄격 모드 동작
  • 새로운 훅

자동 배칭

배치란 여러 상태 업데이트를 한 번에 묶어 처리하는 작업을 뜻한다.

기존 리액트 17버전 이하에서는 이벤트의 콜백, PromisesetTimeout 같은 곳에서의 상태 업데이트 등 일부 예외 상황에 대해 동기적으로 처리되었다.

function handleClick(){
    setA(a => a + 1) // 상태 변화 없음
    setB(b => b + 1) // 상태 변화 없음
    // 위 두 상태는 묶어서 배치 처리
    setTimeout(() => {
        setA(a => a + 1) // 상태 변화 (리렌더링 발생)
        setB(b => b + 1) // 상태 변화 (리렌더링 발생)
    })
}

하지만 리액트 18 버전에서 추가된 자동 배칭이 적용되면 PromisesetTimeout 같은 곳도 전부 한 번에 처리하게 된다.

이는 예외상황에서 동기적으로 처리되어 여러번 리렌더링 되는 것을 방지한다.

단, 동기적인 업데이트가 필요한 경우에는 새로 추가된 flushSync를 사용하여 동기적으로 처리할 수 있다.

function handleClick(){
    flushSync(() => {
        setA(a => a + 1)
    }) // 상태 변화 즉시 리렌더링
}

이를 위해서는 ReactDOM.render 대신 새로 추가된 ReactDOM.createRoot 를 사용해야하는 조건이 있다.

전환(Transition) 도입

리액트 18에서는 새로운 동시성 기능으로 긴급 업데이트와 전환 업데이트를 구분한다.

  • 긴급 업데이트 : 입력, 클릭 등 직접적인 상호 작용이 바로 반영되어야 하는 경우
  • 전환 업데이트 : UI를 다른 뷰로 전환하는 경우

useTransitionstartTransition을 통해 사용할 수 있다

startTransition 로 감싸진 업데이트는 전환 업데이트로 다른 긴급 업데이트가 발생하면 취소된다.

취소가 되는 경우 기존의 업데이트는 취소되며 새로운 전환 업데이트만 적용되어 최신 업데이트가 반영된다.

전환 업데이트는 백그라운드로 진행되는 동안 현재 콘텐츠를 그대로 표기하며 Suspensefallback을 보지 않게 된다.

이는 사용자가 데이터를 업데이트 하는 동안 기존의 데이터를 계속 볼 수 있게 해준다.

// isPending은 전환이 실행 중이며 아직 보류인 상태인지를 나타냄
const [isPending, startTransition] = useTransition()

function handleClick(){
    startTransition(() => { // 이 함수는 전환 업데이트로 긴급하지 않게 여겨짐
        setA(a => a + 1)
    })
}

Suspense의 새 기능

새로운 기능

스트리밍을 통한 서버사이드 렌더링 지원

기존에는 서버 렌더링 중에 구성요소가 중지 되면 React에서는 하드 오류가 발생했다.

이는 NextRemix 같은 SSR 도구에서는 코드 분할에 Suspense를 사용할 수 없게 만든다.

리액트 18에서는 새로운 서버 렌더러를 도입하여 문자열을 생성하는 대신 스트림을 생성할 수 있게한다.

또한 새로운 렌더러는 Suspense와 완벽 호환되어 아직 준비되지 않은 부분의 fallback 콘텐츠를 내보낸다.

전환을 통한 기존 데이터 숨김 방지

위에서 언급한대로 한번 그려진 데이터가 있고 전환 업데이트로 데이터가 변하는 경우 데이터가 업데이트 되는 동안 유저는 기존 데이터를 보게된다.

이는 사용자 경험으로 하여금 새로 데이터를 받는 동안 스피너 같은 로딩 화면을 보지 않게 한다.

동작 변경

커밋된 트리는 항상 일관성을 가짐

{
    data && 
	<Suspense fallback={<Spinner/>}>
        <Panel>
        	<Content/>
        </Panel>
    </Suspense>
}

위 구조에서 기존 리액트의 동작 방식은 아래와 같다.

  1. Panel을 DOM에 추가하고, holes 데이터로 처리한 뒤 display:none을 넣어 보이지 않게 한다.
  2. Spinner를 DOM에 추가한다.
  3. 실제로 Panel이 마운트 되었기 때문에 레이아웃 이펙트를 실행한다.
  4. Content 의 업데이트를 기다리고 이후 리렌더링한다.
  5. Spinner를 DOM에서 제거한다
  6. display: none을 없앤다.

하지만 새로운 방식에선 아래와 같다.

  1. Spinner를 DOM에 추가한다.
  2. Content의 업데이트를 기다리고 이후 리렌더링한다.
  3. Spinner를 제거한다
  4. PanelContent를 DOM에 추가한다.
  5. Panel의 레이아웃 이펙트를 실행한다 (이동됨)

이에 따라 실제 돔 트리는 실제 렌더링 여부와 동일하게 처리된다.

기존에는 Panel이 아직 그려질 준비가 되지 않았음에도 display:none으로 미리 그려지기 때문에 componentWillMount와 관련하여 여러 이슈를 만들었다.

이 업데이트에서는 이를 해결할 수 있으며, 실제로 일관되게 트리가 유지되는 장점이 있다.

콘텐츠가 나타날 때마다 레이아웃 이펙트 재실행

위의 변경을 토대로 실제로 Panel의 레이아웃 이펙트의 실행 지점이 마지막으로 옮겨졌다.

또한 Panel의 추가가 실제로 데이터가 업데이트 되는 동안에만 일어나기 때문에 Spinner가 활성인 동안에는 Panel은 DOM에 존재하지 않는다.

따라서 Suspense전환 업데이트가 아닌 일반 업데이트가 처리되는 경우 Spinner가 추가되며 Panel은 다시 사라지게 된다.

이로 인해 매 업데이트마다 Panel이 다시 DOM에 추가되며 그때마다 레이아웃 이펙트를 실행하게 된다.

새로운 클라이언트 및 서버 렌더링 API

클라이언트 API

  • createRoot : 기존의 ReactDOM.render를 대신한다. 리액트 18의 최신기능은 createRoot와 함께 쓸 때만 동작한다.
  • hydrateRoot : 기존의 ReactDOM.hydrate를 대신한다. 역시 최신 기능은 hydrateRoot와 함께 쓸 때만 동작한다.

createRoothydrateRootonRecoverableError 라는 새 옵션을 제공한다.

이는 리액트가 로깅을 위해 렌더링 또는 하이드레이션 하는 동안 복구한 에러를 전달 받을 수 있게 한다.

기존적으로 reportError를 통해 전달되며, 오래된 브라우저에서는 console.error를 사용한다.

서버 API

  • renderToPipeableStream : Node 환경에서의 스트리밍용으로 사용한다.
  • renderToReadableStream : DenoCloudflare Workers 등의 Edge Runtime에서 사용한다.

기존의 renderToString은 계속 지원되나, 권장되지 않는다.

새로운 엄격 모드

리액트는 상태를 유지하면서 UI를 추가하거나 제거하는 기능을 추가하려고 한다.

예를 들면 화면에서 이동을 했다가 다시 뒤로가기를 누르면 이전의 상태를 가진채로 렌더링 하는 것이 있다.

이를 위해 마운트 해제 전과 동일한 컴포넌트 상태를 사용하여 다시 마운트 하는 기능을 지원한다.

이는 더 나은 성능을 제공하지만 컴포넌트가 mount/unmount가 여러번 되는 것에 유연하게 대응해야 한다.

개발자는 종종 destroy callback에서 subscription등을 제대로 정리하지 않거나 한 번만 사용된다고 가정하고 개발하는 경우가 있다.

이런 문제를 해결하기 위해 개발 모드에서 엄격모드를 사용하면 기본적으로 마운트가 두번 일어나게 변합니다.

  • 기존

    • 컴포넌트 마운트
      • 레이아웃 이펙트 발생
      • 이펙트 발생
  • 변경

    • 컴포넌트 마운트
      • 레이아웃 이펙트 발생
      • 이펙트 발생
    • 컴포넌트 언마운트
      • 레이아웃 이펙트 제거
      • 이펙트 제거
    • 컴포넌트 마운트
      • 레이아웃 이펙트 발생
      • 이펙트 setup 코드 실행

마운트 해제 및 다시 마운트에는 아래 동작이 포함된다.

  • componentDidMount
  • componentWillUnMount
  • useEffect
  • useLayoutEffect
  • useInsertionEffect

단 이는 개발 모드에만 적용되며 프로덕션에는 적용되지 않는다.

자세한 내용은 github discussion 에서 확인 가능하다.

새로운 훅

리액트에서는 몇가지 새로운 훅을 제공한다

훅 종류

useId

클라이언트와 서버 간의 hydration 불일치를 해결하기 위한 유니크 아이디를 사용한다.

이는 고유 아이디를 사용해 접근성 API를 사용하는 컴포넌트 라이브러리에 유용하다.

또한 새로 변경된 스트리밍 서버 렌더러가 순서 없이 전달하는 방식에서도 중요하게 사용된다.

주의점으로 이는 리스트의 key를 위해 사용하면 안된다.

useTransition

위에서 언급한 전환 업데이트를 사용하기 위해 사용한다.

startTransition 에서 업데이트 되지 않는 내용은 전부 긴급 업데이트로 분류한다.

useDeferredValue

급하지 않은 값에 대한 업데이트를 지연되게 처리할 수 있게 해준다.

디바운싱과 비슷하지만 고정된 딜레이가 없고 리액트가 첫 렌더링 이후 지연된 렌더링을 시도한다.

이는 중단이 가능하며 사용자의 입력을 차단하지 않는다.

useSyncExternalStore

선택적 하이드레이션 및 시간 분할 같은 동시성 렌더링 기능을 사용하는 외부 데이터 소스로 부터의 데이터 수집을 위한 훅이다.

여기서 말하는 외부 데이터 소스는 Redux, MobX, Recoil 등의 외부 상태 관리 라이브러리를 말한다.

리액트는 외부 데이터 소스의 상태를 관찰하지 않기 때문에, 모든 컴포넌트가 동일한 상태를 보지 않는 tearing이 발생할 수 있다.

이를 해결하기 위해 추가된 훅으로 보인다.

useInsertionEffect

이는 CSS-in-JS 라이브러리가 렌더링에 스타일을 주입하는 과정에서 성능 문제를 개선하는 훅이다.

이는 DOM이 변경된 후 실행되지만 layoutEffect가 새 레이아웃을 읽기 전에 호출된다.

이는 라이브러리를 위한 이펙트로 일반적으론 거의 사용되지 않지만 리액트 18에서 동시 렌더링 중 브라우저에게 레이아웃을 다시 계산할 기회를 준다.


Written by@[esllo]
plain developer

GitHubTwitterLinkedIn